<?php
namespace Lavender;

use Forxx\Api\Utils;

class WebPage
{
	/**
	 * request methods
	 * @const
	 */
	const M_RAW 	= 0b1;
	const M_GET 	= 0b10;
	const M_POST 	= 0b100;
	const M_COOKIE 	= 0b1000;
	const M_FILE 	= 0b10000;
	const M_ENV 	= 0b100000;
	const M_SERVER 	= 0b1000000;
	const M_JSON 	= 0b100000000;
	const M_PUT 	= 0b1000000000;
	const M_DELETE 	= 0b10000000000;

	/**
	 * session cookie name
	 * @const
	 */
	const TOKEN_KEY = '_token';
	const CONTENT_TYPE = 'text/html';

	public static $instance;

	/**
	 * session data
	 * @var \Lavender\Session $session
	 */
	public $session;

	//mvc options
	public $options;

    //需要验证授权
    protected $need_auth = true;

    //需要验证签名
    protected $need_sign = true;

	//actions don't need login
	protected $without_auth_actions = array();

    //actions don't need verify token
    protected $without_sign_actions = array('*');

    protected $admin_manager = false;

    protected $sign_field_name = 'sign';
    protected $sign = '';

	//current module & action
	protected $module_name;
	protected $action_name;
    protected $view_path = '';
    protected $uid = 0;

	protected static $start_time;
	protected static $end_time;
	protected static $_overtime = 0.2; // 超过n秒的接口，计入log中

    protected static $_raw_body = null;

	public function __construct($module_name = null, $options = array())
	{
		$this->module_name = $module_name;
		$this->options = $options;
        if(empty($this->view_path))
        {
            $this->view_path = rtrim($module_name,'_page');
        }
        if(isset($options['sign_field'])){
            $this->sign_field_name = $options['sign_field'];
        }
	}

    /**
     * run the web application
     * @param $web_options
     * @throws Exception
     */
	public static function run($web_options)
	{
        /**
		 * work环境不显示trace，但将耗时较高的存进log中
		 * 体现在 success() , error()中
		 * wuzhenqiu
		 */
		self::$start_time = microtime(true);
// 		if(L_ENV == 'develop' || L_ENV == 'test' || L_ENV == 'work') {self::$start_time = microtime(true);}
		
		if (!is_array($web_options['action_modules']) ) {
				throw new Exception('action_modules not setted in $options', Errno::PARAM_INVALID);
		}

		//get route module & action
		$route_options = self::get_route_options();

		$module_name = empty($route_options['module']) ? 'index' : $route_options['module'];
		$action_name = empty($route_options['action']) ? 'index' : $route_options['action'];
		//module not found
		if (!in_array($module_name, $web_options['action_modules'])) {
            if(isset($_SERVER['CONTENT_TYPE']) && strpos(strtolower($_SERVER['CONTENT_TYPE']),"json") !== false){
                $web = new WebService();
                $web->render_error("api not found",404);
            }
            else{
                $web = new WebPage();
                $web->header_notfound();
                $web->render_error("页面未找到或者无权限访问!");
                //echo "{$module_name} , {$action_name} Action module not found.\n";
            }
			return ;
		}

		//execute
		$class_name = "App\\Action\\{$module_name}";
		self::$instance = new $class_name($module_name, $web_options);
		self::$instance->execute($action_name);
	}

	public static function get_route_options()
	{
		$route = ['module'=>'','action'=>''];
		if(Router::check())
		{
			$route['module'] = Router::getModule();
			$route['action'] = Router::getAction();
		}
		return $route;
// 		$m = empty($_GET['action']) ? '' : $_GET['action'];
// 		$m = strpos($m, '/') ? explode('/', $m) : explode('.', $m);

// 		return array(
// 			'module' => trim($m[0]),
// 			'action' => empty($m[1]) ? '' : trim($m[1]),
// 		);
	}

	public function before_execute()
	{
		//token, for csrf
		if (!$this->get_cookie(self::TOKEN_KEY)) {
			$this->set_cookie(self::TOKEN_KEY, $this->make_token());
		}
        if($this->need_auth){
            if(array_search($this->action_name, $this->without_auth_actions) !== false || array_search('*', $this->without_auth_actions) !== false){
                $this->need_auth = false;
            }
        }
        if($this->need_sign){  
            if(array_search($this->action_name, $this->without_sign_actions) !== false || array_search('*', $this->without_sign_actions) !== false){
                $this->need_sign = false;
            }
        }
	}

	public function execute($action_name)
	{
		$this->header_content_type(static::CONTENT_TYPE);
        $this->action_name = $action_name;
		try {
			$this->before_execute();
			$action_method = "{$action_name}_action";
			//action not found
			if (!method_exists($this, $action_method)) {
				$this->header_notfound();
                throw new Exception('action '.$action_name.' not found!');
				return ;
			}
            //检查访问的合法性
            $this->check_sign();

            //检查授权情况
            $this->check_auth();

            $this->check_req_time();

			//call action
			$data = $this->$action_method();

			//render
			return $this->render($data);
		}
		catch (Exception\Auth $e) {
            $session_key = Core::get_config('const','session_key');
			$this->set_cookie($session_key, '',isset($this->options['session_timeout'])?$this->options['session_timeout']:0,'/','.'.Core::get_config('const','domain')['base']);
			return $this->render_auth_error($e->getMessage(), $e->getCode());
		}
		catch (Exception $e) {
			Core::log('slow_error', date('Y-m-d H:i:s') . ', standard: ' . self::$_overtime . ' , uid: ' . $this->uid .  ', ip: ' . $this->get_client_ip() . " error code: " . $e->getCode(). " error message: \n".$e->getTraceAsString()."\n");
			return $this->render_error($e->getMessage(), $e->getCode());
		}
	}

    /**
     * render view
     * @param mixed $__data__ gdata/view is the system key,if set 'view',will render to this view
     * @throws Exception
     */
	public function render($__data__ = null)
	{
		//get friend message
		if (isset($__data__['code']) && $__data__['code'] > 0) {
			//modified by nietao 2013-10-29 自定义错误可以通过
//			$__data__['msg'] = Core::get_lang_text($__data__['code']);
			$msg = Core::get_lang_text($__data__['code']);
		
			if(!empty($msg))
			{
				$__data__['msg'] = $msg;
			}
		}

		//extract data
		if ($__data__) {
			if (! is_array($__data__)) {			 
				throw new Exception("Data to render not an array", Errno::PARAM_INVALID);
			}
			extract($__data__);
		}
		//global data in view
		$gdata = [
			'module' => $this->module_name,
			'action' => $this->action_name,
			'session' => $this->session,
			'options' => $this->options,
			'instance' => $this,
		];

		//set view
		if (empty($view)) {
			if (!empty($code)) {
				$view = 'common/error';
			}
			else {
				$view = $this->view_path . '/' . $this->action_name;
			}
		}

		if (!file_exists(L_APP_PATH . "view/{$view}.php") ) {
			throw new Exception("View not found:{$view}", Errno::FILE_NOTFOUND);
		}
		include_once(L_APP_PATH . "view/{$view}.php");
	}

    public function render_error($msg, $code = -100)
	{
		$data = array(
			'code' => $code,
			'msg' => $msg,
			'view' => 'common/error',
		);

		return $this->render($data);
	}

    public function render_auth_error($msg, $code = 401)
	{
		$data = array(
			'code' => $code,
			'msg' => $msg,
			'view' => 'common/error',
		);

		return $this->render($data);
	}

    public function success($msg = '', $data = null)
	{
		$result['code'] = 0;
		$result['msg'] = $msg;
		$result['data'] = $data;

		self::$end_time = microtime(true);
		$exec_time = self::$end_time - self::$start_time;
		
		$trace = $this->module_name. '.' . $this->action_name . ' , ' . $exec_time;
		if (L_ENV == 'develop' || L_ENV == 'test') {
			$result['trace'] = $trace;
		}
		
		if ($exec_time > self::$_overtime) {
			Core::log('slow_success', date('Y-m-d H:i:s') . ', standard: ' . self::$_overtime . ' , uid: ' . $this->uid . ', ip: ' . $this->get_client_ip() . ', ' . $trace . "\n");
		}
		
		return $result;
	}

    public function error($msg, $code = -1)
	{
		$result['code'] = $code;
		$result['msg'] = $msg;
		
		self::$end_time = microtime(true);
		$exec_time = self::$end_time - self::$start_time;
		
		$trace = $this->module_name. '.' . $this->action_name . ' , ' . $exec_time;
		if (L_ENV == 'develop' || L_ENV == 'test') {
			$result['trace'] = $trace;
		}
		
		if ($exec_time > self::$_overtime) {
			Core::log('slow_error', date('Y-m-d H:i:s') . ', standard: ' . self::$_overtime . ' , uid: ' . $this->uid .  ', ' . $trace . "\n");
		}
		return $result;
	}

	protected function make_token()
	{
		return mt_rand(1000000000, 9999999999);
	}

    /**
     * @return array
     */
    protected function get_request_param()
    {
        $request = array();
        if (!empty($_GET)) {
            foreach ($_GET as $key => $value) {
                $request[$key] = $value;
            }
        }
        if (!empty($_POST)) {
            foreach ($_POST as $key => $value) {
                $request[$key] = $value;
            }
        }
        return $request;
    }

	/**
	 * get request parameters
	 *
	 * @param array $definition {key1:filter1, key2:filter2, ... }
	 * @param int $method request method, self::M_GET / self::M_POST / self::M_COOKIE / self::M_REQUEST / self::M_FILE / self::M_ENV / self::M_SERVER
	 * @param boolean $required
	 * @param string $prefix
	 *
	 * @return array
	 */
	protected function parameters($definition, $method = WebPage::M_GET, $required = false, $prefix = null)
	{
		$source = $this->get_request_source($method);
		if($source === false)
		{
			return false;
		}
		$parameters = array();
        try{
            foreach ($definition as $key => $filter) {
                if(isset($filter['required'])){
                    $required = $filter['required'];
                }
                if (isset($source[$key]) ) {
                    $errmsg = "";
                    $result = Filter::filter($source[$key], $filter,$key,$required);
                }
                else {
                    if ($required) {
                        throw new Exception\Filter("Parameter '{$key}' is required", Errno::INPUT_PARAM_MISSED);
                    }
                    continue;
                }

                //parameter key prefix
                if ($prefix) {
                    $key =  $prefix . $key;
                }
                $parameters[$key] = $result;
            }
        }catch (Exception $e){
            throw new Exception\Filter($e->getMessage(), Errno::INPUT_PARAM_INVALID);
        }
		return $parameters;
	}
	
	protected function get_request_source($method)
	{
		$source = false;
		switch ($method) {
			case null:
			case self::M_GET:
				$source = $_GET;
				break;
			case self::M_POST:
				$source = $_POST;
				break;
			case self::M_COOKIE:
				$source = $_COOKIE;
				break;
			case self::M_FILE:
				$source = $_FILES;
				break;
			case self::M_ENV:
				$source = $_ENV;
				break;
			case self::M_SERVER:
				$source = $_SERVER;
				break;
			case ($method & self::M_RAW) == self::M_RAW:
				$source = Static::get_raw_body();
			case  ($method & self::M_JSON) == self::M_JSON:
				$dist = $source ? $source : $_POST;
				$source = json_decode($dist,true);
				break;
			default:
				return false;
		}
		return $source;
	}
	
	protected static function get_raw_body()
	{
		if(empty(self::$_raw_body))
		{
			self::$_raw_body = file_get_contents("php://input");
		}
		return self::$_raw_body;
	}

    protected function check_sign()
    {
        if($this->need_sign){
            $sign_field_name = $this->sign_field_name;
            $req = array(
                'app_id' => Filter::T_STRING,
                'ver' => Filter::T_STRING,
                $sign_field_name => Filter::T_STRING,
            );
            $params = $this->parameters($req, self::M_GET, true);
            $this->sign = $params[$sign_field_name];
            $request = $this->get_request_param();
            $token = $this->get_cookie(self::TOKEN_KEY);
            if(empty($token))
            {
                throw new Exception\Auth('token missed', Errno::AUTH_FAILED);
            }
            if(empty($this->sign))
            {
                throw new Exception\Auth($sign_field_name.' missed', Errno::AUTH_FAILED);
            }
            if(!$this->is_valid_signature($token, $request, $this->sign))
            {
                throw new Exception\Auth('sign verify failed : '.$this->sign, Errno::AUTH_FAILED);
            }
        }
    }

    /**
     * 验证签名是否正确
     * @param $key
     * @param $request_param
     * @param $signature
     * @return bool
     * @throws Exception
     */
    protected function is_valid_signature($key, $request_param, $signature)
    {
        $signatureClass = isset($this->options['signature'])?$this->options['signature']:'\Lavender\MD5Signature';
        if(!class_exists($signatureClass))
        {
            throw new Exception("web config signature is error!");
        }
        $signature_obj = new $signatureClass($key, '', array('sign' => 1));
        if(!$signature_obj instanceof ASignature)
        {
            throw new Exception("web config signature type is error!");
        }
        $sign = $signature_obj->get_signature($request_param);
        if ($signature === $sign) {
            return true;
        }
        return false;
    }

    protected function check_auth()
    {
        //create session
        $this->session = $this->create_session();
        $is_pass = false;
        if($this->session->is_valid())
        {
            $is_pass = true;
            $id = intval($this->session->getId());
            if(!empty($id))
            {
                $this->uid = $id;
            }
        }
        if((!$is_pass) && $this->need_auth)
        {
            throw new Exception\Auth('auth verify failed', Errno::AUTH_FAILED);
        }
    }

    public function check_req_time(){

    }

	protected function create_session()
	{
        $session_key = Core::get_config('const','session_key');
		$key = $this->get_cookie($session_key);
		$timeout = isset($this->options['session_timeout']) ? $this->options['session_timeout'] : 3600;
		$dao_name = isset($this->options['session_dao']) ? $this->options['session_dao'] : '';

		return new Session($key, $this->get_request_time(), $timeout, $dao_name);
	}

    public function login($uid,$token = null,$setCookie = true)
    {
        $timeout = isset($this->options['session_timeout']) ? $this->options['session_timeout'] : 3600;
        $dao_name = isset($this->options['session_dao']) ? $this->options['session_dao'] : '';
        $request_time = $this->get_request_time();
        if(empty($token)){
            $token = Utils::makeFormatToken(Core::get_config('const','session_hash_key'));
        }
        $this->session = new Session($token,$this->get_request_time(),$timeout,$dao_name);
        $session_key = Core::get_config('const','session_key');
        $this->session->login($uid,$timeout);
        if($setCookie){
            $this->set_cookie($session_key,$token,time() + $timeout);
        }
        return $token;
    }

    protected function logout()
    {
        $this->session->destroy();
        $session_key = Core::get_config('const','session_key');
        $this->set_cookie($session_key,null);
    }

	public function get_current_url()
	{
		$protocol = empty($_SERVER['HTTPS']) ? 'http' : 'https';
		return $protocol . '://' . getenv('HTTP_HOST') . getenv('REQUEST_URI');
	}

	public function get_client_ip()
	{
        if (getenv('HTTP_X_FORWARDED_FOR') && strcasecmp(getenv('HTTP_X_FORWARDED_FOR'), 'unknow')) {
            $ip = getenv('HTTP_X_FORWARDED_FOR');
        }
		elseif (getenv('HTTP_CLIENT_IP') && strcasecmp(getenv('HTTP_CLIENT_IP'), 'unknow')) {
			$ip = getenv('HTTP_CLIENT_IP');
		} elseif (getenv('X-Real-Ip') && strcasecmp(getenv('X-Real-Ip'), 'unknow')) {
            $ip = getenv('X-Real-Ip');
        } elseif (getenv('REMOTE_ADDR') && strcasecmp(getenv('REMOTE_ADDR'), 'unknow')) {
			$ip = getenv('REMOTE_ADDR');
		} elseif (isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] && strcasecmp($_SERVER['REMOTE_ADDR'],'unknow')){
			$ip = $_SERVER['REMOTE_ADDR'];
		} else {
			$ip = '0.0.0.0';
		}
		return $ip;
	}

	public function get_request_time()
	{
		return $_SERVER['REQUEST_TIME'];
	}

	public function get_cookie($name)
	{
		return isset($_COOKIE[$name]) ? $_COOKIE[$name] : null;
	}

	public function set_cookie($name, $value, $expire = 0, $path = "/", $domain = null, $secure = false, $httponly = false)
	{
//        if(is_null($domain)){
//            $domain = '.'.Core::get_config('const','domain')['base'];
//        }
		return setcookie($name, $value, $expire, $path, $domain, $secure, $httponly);
	}

	public function url($module, $action, array $parameters = array(), $protocol = 'http')
	{
		//set default to /
		//empty($this->options['uri_path']) && $this->options['uri_path'] = '/';

		$url = "{$protocol}://{$this->options['domain']}{$this->options['uri_path']}/?action={$module}.{$action}";

		if (!empty($parameters)) {
			$url .= '&' . http_build_query($parameters);
		}

		return $url;
	}

    public function get_uid(){
        return $this->uid;
    }

    public function get_module_name(){
        return $this->module_name;
    }

    public function get_action_name(){
        return $this->action_name;
    }

	public function redirect($uri, $http_response_code = null)
	{
		header('Location: ' . $uri, true, $http_response_code);
	}

	public function header_content_type($type = 'text/html', $charset = 'utf-8')
	{
		header("Content-type:{$type}; charset={$charset}");
	}

	public function header_nocache()
	{
		header('Expires: Wed, 11 Jan 1980 05:00:00 GMT');
		header('Cache-Control: no-cache, must-revalidate, max-age=0');
		header('Pragma: no-cache');
	}

	public function header_modified($timestamp)
	{
		header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $timestamp) . ' GMT');
	}

	public function header_notfound()
	{
		$this->header_status(404);
	}

	public function header_status($code, $replace = true)
	{
		$headers = array(
			100 => 'Continue',
			101 => 'Switching Protocols',

			200 => 'OK',
			201 => 'Created',
			202 => 'Accepted',
			203 => 'Non-Authoritative Information',
			204 => 'No Content',
			205 => 'Reset Content',
			206 => 'Partial Content',

			300 => 'Multiple Choices',
			301 => 'Moved Permanently',
			302 => 'Found',
			303 => 'See Other',
			304 => 'Not Modified',
			305 => 'Use Proxy',
			307 => 'Temporary Redirect',

			400 => 'Bad Request',
			401 => 'Unauthorized',
			403 => 'Forbidden',
			404 => 'Not Found',
			405 => 'Method Not Allowed',
			406 => 'Not Acceptable',
			407 => 'Proxy Authentication Required',
			408 => 'Request Timeout',
			409 => 'Conflict',
			410 => 'Gone',
			411 => 'Length Required',
			412 => 'Precondition Failed',
			413 => 'Request Entity Too Large',
			414 => 'Request-URI Too Long',
			415 => 'Unsupported Media Type',
			416 => 'Requested Range Not Satisfiable',
			417 => 'Expectation Failed',

			500 => 'Internal Server Error',
			501 => 'Not Implemented',
			502 => 'Bad Gateway',
			503 => 'Service Unavailable',
			504 => 'Gateway Timeout',
			505 => 'HTTP Version Not Supported'
		);

		if (!isset($headers[$code] ) ) {
			return false;
		}

		if (empty($_SERVER["SERVER_PROTOCOL"]) || ('HTTP/1.1' != $_SERVER["SERVER_PROTOCOL"] && 'HTTP/1.0' != $_SERVER["SERVER_PROTOCOL"]) ) {
			$_SERVER["SERVER_PROTOCOL"] = 'HTTP/1.0';
		}

		header("{$_SERVER["SERVER_PROTOCOL"]} {$code} {$headers[$code]}", $replace, $code);
		return true;
	}
}
